package tagcloud;

import tagcloud.component.Article;
import tagcloud.component.ValueVector;
import tagcloud.component.DrawBox;
import tagcloud.component.Word;
import java.awt.Color;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.List;
import tagcloud.algorithm.mds.MDS;
import tagcloud.delaunay.Delaunay;

public class TagCloud {

    private static final int MAX_WIDTH = 640;
    private static final int MAX_HEIGHT = 480;

    final int minIteraton = 100;
    final int maxIteration = 300;

    DrawBox fixedmaxbox;
    DrawBox[] drawBoxes;
    int image_width;
    int image_height;

    public TagCloud(Article[] articles, int threshold) {

        int numberOfArticles = articles.length;

        //get all words
        List<String> words = new LinkedList<>();
        for (Article a : articles) {
            for (Word w : a.words) {
                if (!words.contains(w.word) && w.frequency > threshold) {
                    words.add(w.word);
                }
            }
        }
        //construct important vectors
        ValueVector[] valueVectors = new ValueVector[words.size()];
        for (int i = 0; i < words.size(); i++) {
            String word = words.get(i);
            valueVectors[i] = new ValueVector(word, numberOfArticles);
            for (int j = 0; j < numberOfArticles; j++) {
                valueVectors[i].setValue(j, articles[j].getWordFrequency(word));
            }
        }

        //compute dissimilarity matrix
        int numberOfWords = words.size();
        double[][] dissimilarityMatrix = new double[numberOfWords][numberOfWords];
        for (int i = 0; i < numberOfWords; i++) {
            for (int j = i; j < numberOfWords; j++) {
                if (i == j) {
                    dissimilarityMatrix[i][j] = 0;
                } else {
                    //compute distance between two vectors
                    float distance = 0;
                    float sqrt1 = 0;
                    float sqrt2 = 0;
                    for (int k = 0; k < numberOfArticles; k++) {
                        distance += valueVectors[i].value[k] * valueVectors[j].value[k];
                        sqrt1 += valueVectors[i].value[k] * valueVectors[i].value[k];
                        sqrt2 += valueVectors[j].value[k] * valueVectors[j].value[k];
                    }
                    double value = distance / (Math.sqrt(sqrt1) * Math.sqrt(sqrt2));
                    dissimilarityMatrix[i][j] = value;
                    dissimilarityMatrix[j][i] = value;
                }
            }
        }

        //apply MDS on dissimilarityMatrix
        double[][] coordinate2D;
        coordinate2D = MDS.classical(dissimilarityMatrix, numberOfWords);

        //repositioning within frame
        double minX = coordinate2D[0][0];
        double maxX = coordinate2D[0][0];
        double minY = coordinate2D[1][0];
        double maxY = coordinate2D[1][0];
        for (int i = 0; i < numberOfWords; i++) {
            if (coordinate2D[0][i] - maxX > 0) {
                maxX = coordinate2D[0][i];
            }
        }
        for (int i = 0; i < numberOfWords; i++) {
            if (coordinate2D[0][i] - minX < 0) {
                minX = coordinate2D[0][i];
            }
        }
        for (int i = 0; i < numberOfWords; i++) {
            if (coordinate2D[1][i] - maxY > 0) {
                maxY = coordinate2D[1][i];
            }
        }
        for (int i = 0; i < numberOfWords; i++) {
            if (coordinate2D[1][i] - minY < 0) {
                minY = coordinate2D[1][i];
            }
        }

        double ratio;
        {
            double deltaX = maxX - minX;
            double deltaY = maxY - minY;

            if (MAX_WIDTH / deltaX > MAX_HEIGHT / deltaY) {
                ratio = MAX_HEIGHT / deltaY;
            } else {
                ratio = MAX_WIDTH / deltaX;
            }
        }

        for (int i = 0; i < numberOfWords; i++) {
            coordinate2D[0][i] *= ratio;
            coordinate2D[1][i] *= ratio;
            coordinate2D[0][i] += MAX_WIDTH / 2;
            coordinate2D[1][i] += MAX_HEIGHT / 2;
        }

        //create objects
        drawBoxes = new DrawBox[numberOfWords];
        BufferedImage bufferedImage = new BufferedImage(MAX_WIDTH, MAX_HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D imageGraphics = (Graphics2D) bufferedImage.getGraphics();
        for (int i = 0; i < numberOfWords; i++) {
            Font f = new Font("Calibir", Font.BOLD, valueVectors[i].getWeight());
            imageGraphics.setFont(f);
            //calculate border size
            FontMetrics fontMetric = imageGraphics.getFontMetrics();
            int boxWidth = fontMetric.stringWidth(words.get(i));
            int boxHeight = fontMetric.getHeight();
            drawBoxes[i] = new DrawBox(words.get(i), f, boxWidth, boxHeight, (int) coordinate2D[0][i], (int) coordinate2D[1][i]);
            drawBoxes[i].descent = fontMetric.getDescent();
        }

        //--find box that has max size
        fixedmaxbox = drawBoxes[0];
        double wmax2 = 0;
        for (DrawBox d : drawBoxes) {
            if (fixedmaxbox.font.getSize() < d.font.getSize()) {
                fixedmaxbox = d;
                wmax2=d.font.getSize();
            }
        }

        //create graph using delaunay triangulation     
        Delaunay delaunay = new Delaunay();
        delaunay.setBoundingBox(0, 0, MAX_WIDTH, MAX_HEIGHT);
        for (DrawBox d : drawBoxes) {
            delaunay.insertDrawBox(d);
        }
        List<DrawBox[]> edges;
        delaunay = new Delaunay();
        for (DrawBox d : drawBoxes) {
            delaunay.insertDrawBox(d);
        }
        edges = delaunay.computeEdges();

        //apply force-directed
        double convergence;
        double newConvergence = 0;
        double kr = MAX_WIDTH * wmax2 * wmax2;
        double eps = kr / 1000;
        int step = 0;

        boolean exit;

        do {
            convergence = newConvergence;
            //do {                
            step++;
            double replusiveValue = 0;
            //apply replusive force
            repulsiveForce(drawBoxes);
            //drawImage(getImage(), +step + "r.png");
            //} while (overlapDetected);
            //calculate convergence
            for (int i = 0; i < drawBoxes.length - 1; i++) {
                for (int j = i + 1; j < drawBoxes.length; j++) {
                    DrawBox b1 = drawBoxes[i];
                    DrawBox b2 = drawBoxes[j];
                    double centerDistanceX = b1.rect.x - b2.rect.x;
                    double centerDistanceY = b1.rect.y - b2.rect.y;
                    double deltaX = Math.abs(Math.abs(centerDistanceX) - (b1.rect.width + b2.rect.width) / 2);
                    double deltaY = Math.abs(Math.abs(centerDistanceY) - (b1.rect.height + b2.rect.height) / 2);
                    if (b1.rect.intersects(b2.rect)) {
                        replusiveValue += kr * (deltaX < deltaY ? deltaX : deltaY);
                    }
                }
            }

            double springValue = 0;
            springForce(edges);
            //calculate convergence
            for (DrawBox[] edge : edges) {
                DrawBox b1 = edge[0];
                DrawBox b2 = edge[1];
                double currentDistance = Point.distance(b1.rect.x, b1.rect.y, b2.rect.x, b2.rect.y);
                springValue += currentDistance * b1.font.getSize() * b2.font.getSize();
            }

            //drawImage(getImage(), step + "s.png");
            newConvergence = springValue + replusiveValue;
            double delta = Math.abs(newConvergence - convergence);
            exit = (delta < eps && step > minIteraton) || step > maxIteration;
            System.out.println("step: " + step + " spring: " + springValue + " repulsive: " + replusiveValue + "delta: " + delta + " eps: " + eps);
            edges = delaunay.computeEdges();
        } while (!exit);

        //apply repulsive force again
        step = 0;
        boolean overlapDetected;
        do {
            step++;
            overlapDetected = repulsiveForce(drawBoxes);
        } while (overlapDetected && step < maxIteration);
    }

    public BufferedImage getImage() {
        //rearrange boxes
        //--find tl and br
        Point tl = new Point(MAX_WIDTH * 2, MAX_HEIGHT * 2);
        Point br = new Point(0, 0);
        for (DrawBox b : drawBoxes) {
            if (tl.x > b.rect.x) {
                tl.x = b.rect.x;
            }
            if (tl.y > b.rect.y) {
                tl.y = b.rect.y;
            }
            if (br.x < b.rect.x + b.rect.width) {
                br.x = b.rect.x + b.rect.width;
            }
            if (br.y < b.rect.y + b.rect.height) {
                br.y = b.rect.y + b.rect.height;
            }
        }
        //--rearrange
        for (DrawBox b : drawBoxes) {
            b.rect.translate(-tl.x, -tl.y);
        }
        image_width = br.x - tl.x;
        image_height = br.y - tl.y;

        BufferedImage bufferedImage = new BufferedImage(image_width, image_height, BufferedImage.TYPE_INT_ARGB);
        Graphics imageGraphics = bufferedImage.getGraphics();
        //draw image with drawboxes
        Color backcolor = new Color(0.5f, 0.5f, 0.5f, 0.05f);
        imageGraphics.setColor(backcolor);
        imageGraphics.fillRect(0, 0, image_width, image_height);
        for (DrawBox box : drawBoxes) {
            imageGraphics.setColor(Color.BLACK);            
            imageGraphics.setColor(randomColor());
            imageGraphics.setFont(box.font);
            imageGraphics.drawString(box.word, box.rect.x, box.rect.y + box.rect.height - box.descent);
        }
        return bufferedImage;
    }
    
    private Color randomColor()
    {
        return new Color((float)Math.random(), (float)Math.random(), (float)Math.random(), (float)1.0);
    }

    private void springForce(List<DrawBox[]> edges) {
        for (DrawBox[] edge : edges) {
            DrawBox b1 = edge[0];
            DrawBox b2 = edge[1];
            if (!b1.rect.intersects(b2.rect)) {                
                int dx = 1;
                int dy = 1;
                int movex;
                int movey;
                DrawBox boxToMove;
                DrawBox boxFix;
                if (b1.equals(fixedmaxbox)) {
                    boxToMove = b2;
                    boxFix = b1;
                } else if (b2.equals(fixedmaxbox)) {
                    boxToMove = b1;
                    boxFix = b2;
                } else {
                    double distanceFromB1ToMax = Point.distanceSq(b1.rect.x, b1.rect.y, fixedmaxbox.rect.x, fixedmaxbox.rect.y);
                    double distanceFromB2ToMax = Point.distanceSq(b2.rect.x, b2.rect.y, fixedmaxbox.rect.x, fixedmaxbox.rect.y);
                    boxToMove = (distanceFromB1ToMax < distanceFromB2ToMax) ? b2 : b1;
                    boxFix = (distanceFromB1ToMax < distanceFromB2ToMax) ? b1 : b2;
                }
                movex = (boxToMove.rect.getCenterX() < boxFix.rect.getCenterX()) ? dx : -dx;
                movey = (boxToMove.rect.getCenterY() < boxFix.rect.getCenterY()) ? dy : -dy;

                boxToMove.rect.translate(movex, movey);
                //undo if colision
                if (boxToMove.rect.intersects(boxFix.rect)) {
                    boxToMove.rect.translate(-movex, -movey);
                }
            }
        }
    }

    private int countColision(DrawBox[] drawBoxes) {
        int result = 0;
        for (int i = 0; i < drawBoxes.length - 1; i++) {
            for (int j = i + 1; j < drawBoxes.length; j++) {
                if (i != j) //check if two boxes overlap each other
                {
                    DrawBox b1 = drawBoxes[i];
                    DrawBox b2 = drawBoxes[j];
                    if (b1.rect.intersects(b2.rect)) {
                        result++;
                    }
                }
            }
        }
        return result;
    }

    private boolean repulsiveForce(DrawBox[] drawBoxes) {
        boolean overlapDetected = false;
        for (int i = 0; i < drawBoxes.length - 1; i++) {
            for (int j = i + 1; j < drawBoxes.length; j++) {
                if (i != j) //check if two boxes overlap each other
                {
                    DrawBox b1 = drawBoxes[i];
                    DrawBox b2 = drawBoxes[j];
                    if (b1.rect.intersects(b2.rect)) {
                        //colision
                        overlapDetected = true;
                        //seclude twos box in respect of center direction
                        Rectangle r = b1.rect.intersection(b2.rect);

                        int dx = r.width;
                        int dy = r.height;// = 0;// = r.height / 2+1;
                        int movex;
                        int movey;

                        int coli1;
                        int coli2;
                        DrawBox boxFix;
                        DrawBox boxToMove;
                        if (b1.equals(fixedmaxbox)) {
                            boxFix = b1;
                            boxToMove = b2;
                        } else if (b2.equals(fixedmaxbox)) {
                            boxFix = b2;
                            boxToMove = b1;
                        } else {
                            double distanceFromB1ToMax = Point.distanceSq(b1.rect.x, b1.rect.y, fixedmaxbox.rect.x, fixedmaxbox.rect.y);
                            double distanceFromB2ToMax = Point.distanceSq(b2.rect.x, b2.rect.y, fixedmaxbox.rect.x, fixedmaxbox.rect.y);
                            boxToMove = (distanceFromB1ToMax < distanceFromB2ToMax) ? b2 : b1;
                            boxFix = (distanceFromB1ToMax < distanceFromB2ToMax) ? b1 : b2;
                        }
                        movex = (boxToMove.rect.getCenterX() < boxFix.rect.getCenterX()) ? -dx : dx;
                        movey = (boxToMove.rect.getCenterY() < boxFix.rect.getCenterY()) ? -dy : dy;
                        //try move horizontal
                        boxToMove.rect.translate(movex, 0);
                        coli1 = countColision(drawBoxes);
                        boxToMove.rect.translate(-movex, 0);
                        //try move vertical
                        boxToMove.rect.translate(0, movey);
                        coli2 = countColision(drawBoxes);
                        boxToMove.rect.translate(0, -movey);
                        if (coli1 < coli2) {
                            boxToMove.rect.translate(movex, 0);
                        } else if (coli1>coli2){
                            boxToMove.rect.translate(0, movey);
                        }
                        else
                        {
                            if (movex<movey)
                            {
                                boxToMove.rect.translate(movex, 0);
                            }
                            else
                            {
                                boxToMove.rect.translate(0, movey);
                            }
                        }                                
                    }
                }
            }
        }
        return overlapDetected;
    }
}
